/* ----------------------------------------------------------------------------

	pax.widget.autocomplete.js Copyright (C) 2002, 2005, 2008 Mikkel Bergmann, Pointful

	Licence
	-------
	
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

	See lgpl.txt for licence details

---------------------------------------------------------------------------- */
/*
	Script: pax.widget.autocomplete
		This is an ajax based auto-complete text box
		
	Author:
		Mikkel Bergmann, <http://www.pointful.com>
*/

var pax = pax || {};
pax.widget = pax.widget || {};
pax.widget.autocomplete = pax.widget.autocomplete || {};

/*	Method: pax.widget.autocomplete.init
	Initialises an autocomplete widget
	
	Parameters:		
		displayField - the field we display the value in
		args - arguments for the grid widget
		* url - URL to submit the auto completer requests to.
		* searchDelay - Time in ms to delay the search, default is 500ms
		* minSearchLength - Minimum chars to input before searching, default is 1
		* valueField - Optionally you can choose a seperate field to store the data in. By default this is the same as the displayField
		* useIndex - Use the index value rather than the displayed value (like a select dropdown), usually used in conjunction with a hidden displayField; default is false
		* resultMaxLength - Maximum number of results to display, default is 0 = no maximum
		* resultCharWidth - Length of each string to display from the result, default is 20
		* selectResult - Select the non-matching part of the text, for typing over, default is true
		* selectFirstMatch - Automatically selects the first match from the result set, default is true
		* searchMode - Which search mode to be in by default
		* searchModeClasses - An object of the search modes available, format is { 'MODE': { className: 'CSSCLASSNAME', title: 'TITLE' }
		* showConfigIcon - Do we show the configuration icon?
		* buttonOffset - object with x, y pixel offset for the button, eq: { x: 6, y: 8 }, would offset the button 6px right, 8px down

	Returns:
		nothing

	Example:
		(start code)
			Autocomplete widget example: start by typing 'la' in the box below:
			<form name='pAutoCompForm' onsubmit='return false;'>
				<span>
					<input name="town" id="town">
				</span>
			</form>
			This example uses a static php file, with a random selection of 300 Australian town names from NSW. 
			It will show an autocomplete widget where you can quickly find town names, via three diffrent methods:
			<ul>
				<li><b>Start match</b> - matches from the start of the value string</li>
				<li><b>Partial match</b> - matches anywhere within the value string</li>
				<li><b>Sounds like</b> - matches on complete values that sound like the string (using a soundex lookup)</li>
			</ul>
			Click on the icon at the end of the text box, to configure a different search method.
			[:.
				pax.widget.autocomplete.init( pax.$('town'), '/pax/documentation/pax.widget.autocomplete.example.php' );
				exampleCleanup = function() { 
					pax.widget.autocomplete.hideMessageBox( 'town_icon' );
				};
			:]
		(end)
*/
pax.widget.autocomplete.init = function( displayField, args ) {
	// The valueField is the same as the displayField if it has not been specified.
	args.valueField = ( typeof args.valueField != 'undefined' && args.valueField )? args.valueField : displayField;

	var cfg = pax.defAddArgs( {
		valueField: null,								// Optional field to place the value into; use when display value is different to actual value
		url: null,										// URL to submit the auto completer requests to.
		searchDelay: 500,								// Time in ms to delay the search
		minSearchLength: 1,								// Minimum chars to input before searching
		resultMaxLength: 0,								// Maximum number of results to display, 0 = no maximum
		resultCharWidth: 20,							// Length of each string to display from the result
		useIndex: ( args.valueField != displayField ),	// Use the index value rather than the displayed value; always uses index value, if we have a seperate displayFieldID
		selectResult: true,								// Display the matching text as "normal", and the rest as selected text
		selectFirstMatch: true,							// Automatically selects the first match from the result set, default is true
		searchMode: 'default',							// Which search mode to be in by default
		showConfigIcon: true,							// Do we show the configuration icon?
		searchModeClasses: {							// The search modes available.
			'default': { className: 'pAutoCompMatchDefault', title: 'Start match' },
			'partial': { className: 'pAutoCompMatchPartial', title: 'Partial Match' },
			'soundex': { className: 'pAutoCompMatchSoundex', title: 'Sounds like' }
		},
		parseResult: pax.widget.autocomplete.parseResult,				// The default parsing function
		
		// System variables
		lastSearch: '',									// Previous search string
		requestTimer: null,								// The timeout variable.
		result: {},										// The result
		resultCount: 0,									// Count of results
		resultOffset: 0,								// Offset of chosen result.
		resultDiv: 'pAutoCompResult_' + displayField.id,				// The DIV we show the result in.
		originalValue: (args.valueField)? args.valueField.value: ''		// The original value of the field
	}, args );
	
	//	Don't select values, if the searchmode is not the default
	if( cfg.searchMode != 'default' )cfg.selectResult = false;
	
	// Attach config to the displayField
	displayField.pAutoCompConfig = cfg;
	
	// turn off browser auto-complete feature for the fields.
	displayField.setAttribute( 'autocomplete', 'off' );
	args.valueField.setAttribute( 'autocomplete', 'off' );

	// bind various key events
	pax.event.bindKeyUp( displayField, pax.widget.autocomplete.keyUp );
	pax.event.bindKeyPress( displayField, pax.widget.autocomplete.keyDown);
	pax.event.bind( displayField, 'blur', function( event ) {
		var displayField = (event.srcElement)? event.srcElement : event.target;
		var cfg = displayField.pAutoCompConfig;
		pax.widget.autocomplete.setValueAndHide( cfg.resultOffset, displayField );
	} );

	pax.widget.autocomplete.showConfigIcon( displayField.id );
};


/*	Private Method: pax.widget.autocomplete.showConfigIcon
	Shows the configuration icon, depending on the selected mode.
*/
pax.widget.autocomplete.showConfigIcon = function( displayFieldID ) {
	var displayField = pax.$( displayFieldID );
	var cfg = displayField.pAutoCompConfig;
	if( cfg.showConfigIcon ) {
			
		//	Shows the button at the end of the field
		var showButtonFunc = function() {
			
			var pos = pax.util.getPosition( displayField );
			var offsetRight = (cfg.buttonOffset && cfg.buttonOffset.x)? cfg.buttonOffset.x: 0;
			var offsetDown = (cfg.buttonOffset && cfg.buttonOffset.y)? cfg.buttonOffset.y: 0;
			var myBox = pax.box.show( displayField.id + '_icon', cfg, 'pAutoCompConfigIcon', pos.x + pos.width + offsetRight - 19, pos.y + offsetDown );
			
			myBox.innerHTML = '';
	
			//	Turn the box into a button
			pax.widget.button.init( myBox, {
				baseClass: cfg.searchModeClasses[cfg.searchMode]['className'],
				showChrome: false,
				width: '16px',
				height: '14px',
				func: function( e ) {
					pax.widget.autocomplete.showConfigBox( displayField.id );
					return false;
				},
				inline: false
			} );
			//console.log( 'showButtonFunc' );
		};
		
		//showButtonFunc();

		//	New idea: only show it on focus - should hide on blur.
		//	We must check if people are trying to click the configs
		pax.event.bind( displayField, 'focus', showButtonFunc );
		
		//	Need to create a delay, and a way to check if we're showing the config, and then not hide yet.
		pax.event.bind( displayField, 'blur', function() {
			pax.box.hide( displayField.id + '_icon' );
		} );

		
		//	Repositions the button every 300ms, in case the browser changes dimensions, or the DOM changes. 
		//	WIP: MUST USE A SINGLE FUNC FOR THIS! - We're eating up all the CPU with so many timers..
		//	http://localhost/paxjs.com/demo/demo_autocomplete.htm
		
		//window.setInterval( showButtonFunc, 300 );

	

		/*
		var iconId = displayField.id + '_icon';
		
		var myBox = pax.$( iconId );
		
		if( ! myBox ) {
			//	The display field MUST float left for us to position the button correctly
			var style = displayField.style || '';
			style += (style == '')? '': '; ';
			style += 'float: left;';
			displayField.setAttribute( 'style', style );
			
			//	Add the button, to display the calendar.
			var myBox = pax.box.append( iconId, '', 'pAutoCompConfigIcon', displayField, 'E' );
			
			//	We give focus to the field, when the box is clicked; this is necessary for FF, as the box overlaps the field.
			//	There must be a better way to do this? - we MUST append it, or the box goes "wonky" on browser resize / text resize...
			pax.event.bind( myBox, 'click', function() { displayField.focus(); } );
			
			//	Get button offsets in pixels
			var offsetRight = (cfg.buttonOffset && cfg.buttonOffset.x)? cfg.buttonOffset.x: 0;
			var offsetDown = (cfg.buttonOffset && cfg.buttonOffset.y)? cfg.buttonOffset.y: 0;
			
			//	Adjust left
			var left = myBox.style.left;
			left = ( left.indexOf( 'px' ) > -1 )? left.substring( 0, left.indexOf( 'px' ) ): left;
			myBox.style.left = (parseInt(left - 23) + offsetRight) + 'px';
			pax.css.opacity( myBox, 50 );	// Set opacity at 50%, so we can see text through it.
			
			//	Adjust top
			var top = myBox.style.top || '0px';
			top = parseInt( ( top.indexOf( 'px' ) > -1 )? top.substring( 0, top.indexOf( 'px' ) ): top, 10 );
			top = (top < 0)? 0: top;
			
			myBox.style.top = (parseInt(top + 7) + offsetDown) + 'px';
			myBox.style.top = (parseInt(top, 10) + offsetDown) + 'px';
		}
		
		myBox.innerHTML = '';

		//	Turn the box into a button
		pax.widget.button.init( myBox, {
			baseClass: cfg.searchModeClasses[cfg.searchMode]['className'],
			showChrome: false,
			width: '16px',
			height: '14px',
			func: function( e ) {
				pax.widget.autocomplete.showConfigBox( displayField.id );
				return false;
			},
			inline: false
		} );
		*/


			
			
			
			
		
	}
};


/*	Private Method: pax.widget.autocomplete.showConfigBox
	Show the configuration box
*/
pax.widget.autocomplete.showConfigBox = function( displayFieldID ) {
	var displayField = pax.$( displayFieldID );
	var cfg = displayField.pAutoCompConfig;
	var pos = pax.util.getPosition( displayField );
	var iconPos = pax.util.getPosition( pax.$( displayField.id + '_icon' ) );
	var myConfig = "";

	// Add a selection for each configuration
	for( var c in cfg.searchModeClasses ) {
		myConfig += "<div onclick=\"javascript:" +
			" pax.widget.autocomplete.setSearchMode('"+c+"', pax.$('" + displayFieldID + "'));" + 
			" pax.box.hide('" + displayFieldID + "_config');" + 
			" pax.widget.autocomplete.showConfigIcon('" + displayFieldID + "');" +
			" pax.$('" + displayFieldID + "').focus();" +
			" return false;\" " +
			" class='" + cfg.searchModeClasses[c]['className'] + "'" + 
			" style='padding-left: 20px; height: 20px;'>" + 
			cfg.searchModeClasses[c]['title'] + "</div>";
	}

	pax.box.show( displayFieldID + '_config', myConfig, 'pAutoCompConfigBox', pos.x + pos.width, iconPos.y );
};


/*	Private Method: pax.widget.autocomplete.setValueAndHide
	Sets the value, and hides the search box.
*/
pax.widget.autocomplete.setValueAndHide = function( index, displayField ) {
	var cfg = displayField.pAutoCompConfig;
	var valueField = cfg.valueField;
	var disValue = pax.widget.autocomplete.getDisplayResult( displayField );
	var valValue = pax.widget.autocomplete.getValueResult( displayField );
	
	if( disValue != 'undefined' )displayField.value = disValue;
	if( valValue != 'undefined' )valueField.value = valValue;
	pax.widget.autocomplete.hideResult( displayField );
};


/*	Private Method: pax.widget.autocomplete.getIndex
	Get the index result at a specific offset - useful with allowIndex
*/
pax.widget.autocomplete.getIndex = function( displayField ) {
	var cfg = displayField.pAutoCompConfig;
	var count = 0;
		
	for( var r in cfg.result ) {
		if( count == cfg.resultOffset )return r;
		count += 1;
	}
	
	// If we don't get an index, set the original value.
	if( ! cfg.selectFirstMatch )return ( cfg.useIndex )? cfg.originalValue: false;
};


/*	Private Method: pax.widget.autocomplete.getResultOffset
	Get the offset, from the index result
*/
pax.widget.autocomplete.getResultOffset = function( displayField, index ) {
	var cfg = displayField.pAutoCompConfig;
	var count = 0;
	for( r in cfg.result ) {
		if( r == index )return count;
		count += 1;
	}
	return cfg.resultOffset;		// If we don't get an index, return the current result offset.
};


/*	Private Method: pax.widget.autocomplete.getDisplayResult
	Get the selected value for display: always the value, not the key.
*/
pax.widget.autocomplete.getDisplayResult = function( displayField ) {
	var cfg = displayField.pAutoCompConfig;
	var valueField = cfg.valueField;
	var myValue = cfg.result[pax.widget.autocomplete.getIndex( displayField )];
	
	if( typeof( myValue ) == 'undefined' || myValue == false ) {
		return displayField.value;
	} else if( myValue == 'undefined' )myValue = '';	// Note, we will get the string 'undefined' if the result is empty...

	return myValue;
};


/*	Private Method: pax.widget.autocomplete.getValueResult
	Get the selected value: could be either value or key (index)
*/
pax.widget.autocomplete.getValueResult = function( displayField ) {
	var cfg = displayField.pAutoCompConfig;
	var myValue = ( cfg.useIndex )?'' + pax.widget.autocomplete.getIndex( displayField ) : '' + cfg.result[pax.widget.autocomplete.getIndex( displayField )];
	return ( typeof( myValue ) == 'undefined' )? cfg.valueField.value : myValue;
};


/*	Private Method: pax.widget.autocomplete.keyDown
	Events that fire as key is pressed, and before the key is released
*/
pax.widget.autocomplete.keyDown = function( event ) {
	var displayField = (event.srcElement)? event.srcElement : event.target;
	var cfg = displayField.pAutoCompConfig;
	// TAB - set the value of the textbox to the offset value.
	if( event.keyCode == 9 )pax.widget.autocomplete.setValueAndHide( cfg.resultOffset, displayField );
};


/*	Private Method: pax.widget.autocomplete.keyUp
	Events that fire as the key is released
*/
pax.widget.autocomplete.keyUp = function( event ) {
	var displayField = (event.srcElement)? event.srcElement : event.target;
	var cfg = displayField.pAutoCompConfig;	
	var highlight = false;
	
	if( event.keyCode == 40 ) {							// Arrow key down - move down list of options
		if( !pax.isIe ) { event.preventDefault(); }		// Stop keypress from propagating.
		cfg.resultOffset += 1;
		if( cfg.resultOffset > ( cfg.resultCount - 1 ) )cfg.resultOffset = cfg.resultCount -1;
		highlight = true;
	} else if( event.keyCode == 38 ) {					// Arrow key up - move up list of options
		if( !pax.isIe ) { event.preventDefault(); }		// Stop keypress from propagating.
		cfg.resultOffset -= 1;
		if( cfg.resultOffset < 0 )cfg.resultOffset = 0;
		highlight = true;
	} else if( event.keyCode == 13 ) {					// Enter - set the value of the textbox to the offset value.
		if( !pax.isIe ) { event.preventDefault(); }		// Stop keypress from propagating.
		pax.widget.autocomplete.setValueAndHide( cfg.resultOffset, displayField );
		return;
	} else if( event.keyCode == 27 ) {					// ESC - hide list of options
		if( !pax.isIe ) { event.preventDefault(); }		// Stop keypress from propagating.
		displayField.value = cfg.originalValue;
		pax.widget.autocomplete.hideResult( displayField );
		return;
	}

	if( highlight )pax.widget.autocomplete.highlightResult( displayField );
	else {
		if( displayField.value != cfg.lastSearch ) {	// Do the search via a timeout.
			window.clearTimeout( cfg.requestTimer );
			cfg.requestTimer = window.setTimeout( function() { 
				pax.widget.autocomplete.search( displayField ) 
			}, cfg.searchDelay );
		}
	}
};


/*	Method: pax.widget.autocomplete.setSearchMode
	Chooses which search mode to use
	
	Parameters:		
		searchMode - which search mode to use
		displayField - which field has the config, and is used for display

	Returns:
		nothing

	Example:
		(start code)
			Try typing 'Lake cat eye', and you should get 'Lake Cathie'. "Lake cat eye" is what the locals call it ;o)
			<form name='pAutoCompForm' onsubmit='return false;'>
				<span>
					<input name="pax.widget.autocomplete.setSearchMode.town" id="pax.widget.autocomplete.setSearchMode.town">
				</span>
			</form>
			[:.
				pax.widget.autocomplete.init( pax.$('pax.widget.autocomplete.setSearchMode.town'), '/pax/documentation/pax.widget.autocomplete.example.php' );
				pax.widget.autocomplete.setSearchMode( 'soundex', pax.$('pax.widget.autocomplete.setSearchMode.town') );
				pax.widget.autocomplete.showConfigIcon( 'pax.widget.autocomplete.setSearchMode.town' );
				
				exampleCleanup = function() { 
					pax.box.hide( 'pax.widget.autocomplete.setSearchMode.town_icon' );
				};
			:]
		(end)
		This example is almost identical to the <pax.widget.autocomplete.init> example, except it starts in 'sounds like' mode. 
		You should note that we must use pax.widget.autocomplete.showConfigIcon to update the config icon.
*/
pax.widget.autocomplete.setSearchMode = function( searchMode, displayField ) {
	var cfg = displayField.pAutoCompConfig;
	var isDifferentSearchMode = ( searchMode != cfg.searchMode );
	cfg.searchMode = ( typeof searchMode != 'undefined' ) ? searchMode : 'default';		// If the search is to match within text (ie:partially - see .php script for details)
	// Note, we can't use selectResult with special searchMode!
	if( cfg.searchMode != 'default' )cfg.selectResult = false;
	//	Run the search
	if( isDifferentSearchMode ) {
		pax.widget.autocomplete.search( displayField );
	}
};


/*	Private Method: pax.widget.autocomplete.search
	Run the search.
*/
pax.widget.autocomplete.search = function( displayField ) {
	var cfg = displayField.pAutoCompConfig;
	cfg.resultCount = 0;
	cfg.resultOffset = ( cfg.selectFirstMatch )? 0: -1;
	
	if( displayField.value.length >= cfg.minSearchLength ) {
		cfg.lastSearch = displayField.value;
		
		var postString = pax.postString( {
			self: displayField.id,
			searchMode: cfg.searchMode,
			query: displayField.value
		} );
		
		if( cfg.currentPost )pax.cancel( cfg.currentPost );
		cfg.currentPost = pax.post( cfg.url, postString, cfg.parseResult );
	} else {
		pax.widget.autocomplete.hideResult( displayField );
	}
};


/*	Private Method: pax.widget.autocomplete.parseResult
	Parse result, and call the show function.
*/
pax.widget.autocomplete.parseResult = function( xml, txt, url ) {
	var result = pax.unJSON( txt );

	//	We have a pointer sent to the server called __self__, which we then use when the result is passed back.
	var displayField = pax.$( result['__self__'] );
	var cfg = displayField.pAutoCompConfig;
	
	// Remove '__self__' from result
	var myResult = {};
	for( var r in result )if( r != '__self__' )myResult[r] = result[r];

	cfg.result = myResult;											// Keep a copy, for displaying the selected option.
	pax.widget.autocomplete.showResult( displayField, myResult );	// Show the result
	window.clearTimeout( cfg.requestTimer );						// Reset search request timer
};


/*	Private Method: pax.widget.autocomplete.highlightResult
	Highlights the result that is currently selected, and un-highlights the rest.
*/
pax.widget.autocomplete.highlightResult = function( displayField, offset ) {
	var cfg = displayField.pAutoCompConfig;
	var offset = ( offset ) ? offset : cfg.resultOffset;
	var resDiv = pax.$( cfg.resultDiv );
	if( !resDiv )return;	// If the result div hasn't been shown yet, we return.
	// Loop on the divs within the result div, and turn off.
	var myDivs = resDiv.getElementsByTagName('div');
	var count = 0;
	for( var d = 0; d < myDivs.length; d++ ) {
		if( typeof myDivs[d] != 'undefined' ) {	
			if( typeof myDivs[d].id != 'undefined' ) {
				myDivs[d].className = 'pAutoCompResultLine';
				if( count == offset ) {
					myDivs[d].className = 'pAutoCompResultLineSelected';
				}
				count += 1;
			}
		}
	}

	if( cfg.selectResult ) {
		//	Display the matching text as "normal", and the rest as selected text
		var resultValue = pax.widget.autocomplete.getDisplayResult( displayField );
		var matchValue = cfg.lastSearch;	//	Match on the search value, rather than the field value
		if( resultValue != false ) {
			if( resultValue.length > matchValue.length ) {
				displayField.value = resultValue;
				pax.form.selectRange( displayField, matchValue.length, resultValue.length );
			}
		}
		displayField.focus();
	}
};


/*	Private Method: pax.widget.autocomplete.showResult
	Show the result box
*/
pax.widget.autocomplete.showResult = function( displayField, res ) {
	if( res == null )return;
	var cfg = displayField.pAutoCompConfig;
	var myList = '';
	var count = 0;
	for( var r in res ) {
		if( cfg.resultCharWidth > 0 ) {
			resultValue = res[r].substring(0,cfg.resultCharWidth);
			if( resultValue.length > cfg.resultCharWidth - 1 )resultValue += '...';
		} else resultValue = res[r];
		
		// Highlight the matching text in the result value here.
		matchValue = resultValue.toUpperCase();		
		boldStart = matchValue.indexOf( displayField.value.toUpperCase() );
		boldEnd   = displayField.value.length;
		matchValue = resultValue.substring( boldStart, boldStart + boldEnd );
		resultValue = resultValue.substring(0,boldStart) + '<b>' + matchValue  + '</b>' + resultValue.substring(boldStart + boldEnd, resultValue.length);

		var selectJS = " onmouseover=\"pax.$('" + displayField.id + "').pAutoCompConfig.resultOffset = pax.widget.autocomplete.getResultOffset( pax.$('" + displayField.id + "'), " + r + ");pax.widget.autocomplete.highlightResult(pax.$('" + displayField.id + "') );\" ";

		if( count == cfg.resultOffset ) {
			myList += "<div " + selectJS + " class='pAutoCompResultLineSelected' id='" + cfg.resultDiv + "_" + count + "'>" + resultValue + "</div>\n";
		} else {
			myList += "<div " + selectJS + " class='pAutoCompResultLine' id='" + cfg.resultDiv + "_" + count + "'>" + resultValue + "</div>\n";
		}
		count += 1;
		if( cfg.resultMaxLength > 0 && count >= cfg.resultMaxLength )break;
	}
	cfg.resultCount = count;
	
	var pos = pax.util.getPosition( displayField );
	var myBox = pax.box.append( cfg.resultDiv, myList, 'pAutoCompResult' );
	myBox.style.position = 'absolute';
	myBox.style.display = 'block';
	
	myBox.style.left = pos.x + 'px';
	myBox.style.top = parseInt( pos.y + pos.height ) + 'px';
	myBox.style.width = pos.width;
	if( myBox.style.zIndex < 1 )myBox.style.zIndex = 3;  // Ensure that the box has a positive zIndex.

	if( cfg.selectResult ) {
		//	Display the matching text as "normal", and the rest as selected text
		var resultValue = pax.widget.autocomplete.getDisplayResult( displayField );
		var matchValue = cfg.lastSearch;	//	Match on the search value, rather than the field value
		if( resultValue != false ) {
			if( resultValue.length > matchValue.length ) {
				displayField.value = resultValue;
				pax.form.selectRange( displayField, matchValue.length, resultValue.length );
			}
		}
		displayField.focus();
	}
};


/*	Private Method: pax.widget.autocomplete.hideResult
	Hide the result box
*/
pax.widget.autocomplete.hideResult = function( displayField ) {
	var cfg = displayField.pAutoCompConfig;
	pax.box.hide( pax.$( cfg.resultDiv ) );
};